package ar.com.hjg.pngj.pixels;

import java.util.LinkedList;
import java.util.zip.Deflater;

import ar.com.hjg.pngj.FilterType;
import ar.com.hjg.pngj.ImageInfo;

/** Special pixels writer for experimental super adaptive strategy */
public class PixelsWriterMultiple extends PixelsWriter {
  /**
   * unfiltered rowsperband elements, 0 is the current (rowb). This should include all rows of current band, plus one
   */
  protected LinkedList<byte[]> rows;

  /**
   * bank of compressor estimators, one for each filter and (perhaps) an adaptive strategy
   */
  protected CompressorStream[] filterBank = new CompressorStream[6];
  /**
   * stored filtered rows, one for each filter (0=none is not allocated but linked)
   */
  protected byte[][] filteredRows = new byte[5][];
  protected byte[] filteredRowTmp; //

  protected FiltersPerformance filtersPerf;
  protected int rowsPerBand = 0; // This is a 'nominal' size
  protected int rowsPerBandCurrent = 0; // lastRowInThisBand-firstRowInThisBand +1 : might be
  // smaller than rowsPerBand
  protected int rowInBand = -1;
  protected int bandNum = -1;
  protected int firstRowInThisBand, lastRowInThisBand;
  private boolean tryAdaptive = true;

  protected static final int HINT_MEMORY_DEFAULT_KB = 100;
  // we will consume about (not more than) this memory (in buffers, not counting the compressors)
  protected int hintMemoryKb = HINT_MEMORY_DEFAULT_KB;

  private int hintRowsPerBand = 1000; // default: very large number, can be changed

  private boolean useLz4 = true;

  public PixelsWriterMultiple(ImageInfo imgInfo) {
    super(imgInfo);
    filtersPerf = new FiltersPerformance(imgInfo);
    rows = new LinkedList<byte[]>();
    for (int i = 0; i < 2; i++)
      rows.add(new byte[buflen]); // we preallocate 2 rows (rowb and rowbprev)
    filteredRowTmp = new byte[buflen];
  }

  @Override
  protected void filterAndWrite(byte[] rowb) {
    if (!initdone)
      init();
    if (rowb != rows.get(0))
      throw new RuntimeException("?");
    setBandFromNewRown();
    byte[] rowbprev = rows.get(1);
    for (FilterType ftype : FilterType.getAllStandardNoneLast()) {
      // this has a special behaviour for NONE: filteredRows[0] is null, and the returned value is
      // rowb
      if (currentRow == 0 && ftype != FilterType.FILTER_NONE && ftype != FilterType.FILTER_SUB)
        continue;
      byte[] filtered = filterRowWithFilterType(ftype, rowb, rowbprev, filteredRows[ftype.val]);
      filterBank[ftype.val].write(filtered);
      if (currentRow == 0 && ftype == FilterType.FILTER_SUB) { // litle lie, only for first row
        filterBank[FilterType.FILTER_PAETH.val].write(filtered);
        filterBank[FilterType.FILTER_AVERAGE.val].write(filtered);
        filterBank[FilterType.FILTER_UP.val].write(filtered);
      }
      // adptive: report each filterted
      if (tryAdaptive) {
        filtersPerf.updateFromFiltered(ftype, filtered, currentRow);
      }
    }
    filteredRows[0] = rowb;
    if (tryAdaptive) {
      FilterType preferredAdaptive = filtersPerf.getPreferred();
      filterBank[5].write(filteredRows[preferredAdaptive.val]);
    }
    if (currentRow == lastRowInThisBand) {
      int best = getBestCompressor();
      // PngHelperInternal.debug("won: " + best + " (rows: " + firstRowInThisBand + ":" + lastRowInThisBand + ")");
      // if(currentRow>90&&currentRow<100)
      // PngHelperInternal.debug(String.format("row=%d ft=%s",currentRow,FilterType.getByVal(best)));
      byte[] filtersAdapt = filterBank[best].getFirstBytes();
      for (int r = firstRowInThisBand, i = 0, j = lastRowInThisBand - firstRowInThisBand; r <= lastRowInThisBand; r++, j--, i++) {
        int fti = filtersAdapt[i];
        byte[] filtered = null;
        if (r != lastRowInThisBand) {
          filtered =
              filterRowWithFilterType(FilterType.getByVal(fti), rows.get(j), rows.get(j + 1),
                  filteredRowTmp);
        } else { // no need to do this filtering, we already have it
          filtered = filteredRows[fti];
        }
        sendToCompressedStream(filtered);
      }
    }
    // rotate
    if (rows.size() > rowsPerBandCurrent) {
      rows.addFirst(rows.removeLast());
    } else
      rows.addFirst(new byte[buflen]);
  }

  @Override
  public byte[] getRowb() {
    return rows.get(0);
  }


  private void setBandFromNewRown() {
    boolean newBand = currentRow == 0 || currentRow > lastRowInThisBand;
    if (currentRow == 0)
      bandNum = -1;
    if (newBand) {
      bandNum++;
      rowInBand = 0;
    } else {
      rowInBand++;
    }
    if (newBand) {
      firstRowInThisBand = currentRow;
      lastRowInThisBand = firstRowInThisBand + rowsPerBand - 1;
      int lastRowInNextBand = firstRowInThisBand + 2 * rowsPerBand - 1;
      if (lastRowInNextBand >= imgInfo.rows) // hack:make this band bigger, so we don't have a small
                                             // last band
        lastRowInThisBand = imgInfo.rows - 1;
      rowsPerBandCurrent = 1 + lastRowInThisBand - firstRowInThisBand;
      tryAdaptive =
          rowsPerBandCurrent <= 3 || (rowsPerBandCurrent < 10 && imgInfo.bytesPerRow < 64) ? false
              : true;
      // rebuild bank
      rebuildFiltersBank();
    }
  }

  private void rebuildFiltersBank() {
    long bytesPerBandCurrent = rowsPerBandCurrent * (long) buflen;
    final int DEFLATER_COMP_LEVEL = 4;
    for (int i = 0; i <= 5; i++) {// one for each filter plus one adaptive
      CompressorStream cp = filterBank[i];
      if (cp == null || cp.totalbytes != bytesPerBandCurrent) {
        if (cp != null)
          cp.close();
        if (useLz4)
          cp = new CompressorStreamLz4(null, buflen, bytesPerBandCurrent);
        else
          cp =
              new CompressorStreamDeflater(null, buflen, bytesPerBandCurrent, DEFLATER_COMP_LEVEL,
                  Deflater.DEFAULT_STRATEGY);
        filterBank[i] = cp;
      } else {
        cp.reset();
      }
      cp.setStoreFirstByte(true, rowsPerBandCurrent); // TODO: only for adaptive?
    }
  }

  private int computeInitialRowsPerBand() {
    // memory (only buffers) ~ (r+1+5) * bytesPerRow
    int r = (int) ((hintMemoryKb * 1024.0) / (imgInfo.bytesPerRow + 1) - 5);
    if (r < 1)
      r = 1;
    if (hintRowsPerBand > 0 && r > hintRowsPerBand)
      r = hintRowsPerBand;
    if (r > imgInfo.rows)
      r = imgInfo.rows;
    if (r > 2 && r > imgInfo.rows / 8) { // redistribute more evenly
      int k = (imgInfo.rows + (r - 1)) / r;
      r = (imgInfo.rows + k / 2) / k;
    }
    // PngHelperInternal.debug("rows :" + r + "/" + imgInfo.rows);
    return r;
  }

  private int getBestCompressor() {
    double bestcr = Double.MAX_VALUE;
    int bestb = -1;
    for (int i = tryAdaptive ? 5 : 4; i >= 0; i--) {
      CompressorStream fb = filterBank[i];
      double cr = fb.getCompressionRatio();
      if (cr <= bestcr) { // dirty trick, here the equality gains for row 0, so that SUB is prefered
                          // over PAETH, UP, AVE...
        bestb = i;
        bestcr = cr;
      }
    }
    return bestb;
  }

  @Override
  protected void initParams() {
    super.initParams();
    // if adaptative but too few rows or columns, use default
    if (imgInfo.cols < 3 && !FilterType.isValidStandard(filterType))
      filterType = FilterType.FILTER_DEFAULT;
    if (imgInfo.rows < 3 && !FilterType.isValidStandard(filterType))
      filterType = FilterType.FILTER_DEFAULT;
    for (int i = 1; i <= 4; i++) { // element 0 is not allocated
      if (filteredRows[i] == null || filteredRows[i].length < buflen)
        filteredRows[i] = new byte[buflen];
    }
    if (rowsPerBand == 0)
      rowsPerBand = computeInitialRowsPerBand();
  }

  @Override
  public void close() {
    super.close();
    rows.clear();
    for (CompressorStream f : filterBank) {
      f.close();
    }
  }

  public void setHintMemoryKb(int hintMemoryKb) {
    this.hintMemoryKb =
        hintMemoryKb <= 0 ? HINT_MEMORY_DEFAULT_KB : (hintMemoryKb > 10000 ? 10000 : hintMemoryKb);
  }

  public void setHintRowsPerBand(int hintRowsPerBand) {
    this.hintRowsPerBand = hintRowsPerBand;
  }

  public void setUseLz4(boolean lz4) {
    this.useLz4 = lz4;
  }

  /** for tuning memory or other parameters */
  public FiltersPerformance getFiltersPerf() {
    return filtersPerf;
  }

  public void setTryAdaptive(boolean tryAdaptive) {
    this.tryAdaptive = tryAdaptive;
  }

}
